import java.nio.ByteBuffer;
import java.nio.ByteOrder;
import java.util.Objects;
import joe.Audio;
import joe.AudioMini;
import joe.Cleaner;
import joe.Engine;
import joe.Font;
import joe.InputGlfw;
import joe.Mat4;
import joe.Render;
import joe.RenderGles3;
import joe.ResourceManager;
import joe.ResourceMapFile;
import joe.ResourceTexture;
import joe.ResourceVideo;
import joe.SceneCamera;
import joe.SceneModel;
import joe.UnsafeUtil;
import joe.Vec4;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import org.lwjgl.system.MemoryUtil;
import org.tinylog.Logger;

// java: -XX:+UnlockExperimentalVMOptions -XX:+EnableJVMCI --enable-native-access=ALL-UNNAMED --add-opens java.base/jdk.internal.misc=ALL-UNNAMED
public class JoeDemo {
	public static final class DemoModel extends Render.Model {
		public final Mat4 uMVP = new Mat4().identity();
		public final Vec4 uColor = new Vec4().set(0.5f, 0.5f, 0.5f);
		public final @NotNull Render.Texture2D uTex;
		public float uTime;

		public DemoModel() {
			Logger.info("JoeDemo: loading model begin");
			var engine = Engine.current();
			var render = engine.getRender();
			_program = engine.getResourceManager().getOrAddShader("demo.vs", "demo.fs", getClass());
			var vertData = ByteBuffer.allocateDirect((4 * 3 + 4 + 4 * 2) * 4).order(ByteOrder.nativeOrder());
			vertData.putFloat(-0.5f).putFloat(-0.5f).putFloat(0).putInt(0xffffffff).putFloat(0).putFloat(1); // bottom left
			vertData.putFloat(-0.5f).putFloat(+0.5f).putFloat(0).putInt(0xffff00ff).putFloat(0).putFloat(0); // top left
			vertData.putFloat(+0.5f).putFloat(-0.5f).putFloat(0).putInt(0xffffff00).putFloat(1).putFloat(1); // bottom right
			vertData.putFloat(+0.5f).putFloat(+0.5f).putFloat(0).putInt(0xff00ffff).putFloat(1).putFloat(0); // top right
			vertData.flip();
			var vertBuf = render.createVertexBuffer("f3UB4f2", 24, MemoryUtil.memAddress(vertData), vertData.remaining());
			_mesh = render.createMesh(Render.TRIANGLES, vertBuf, engine.getQuadsVertexIndex());
			_count = 6;
			var resTex = Objects.requireNonNull(engine.getResourceManager().load("demo.avif", ResourceTexture.class));
			uTex = render.createTexture2D(resTex.getWidth(), resTex.getHeight(), resTex.getDepth(), 0, resTex.getDataPtr());
//			var resTex = Objects.requireNonNull(engine.getResourceManager().load("demo.dds", ResourceMapFile.class));
//			uTex = render.createTexture2D(64, 64, 8, 0, resTex.getDataPtr() + 0x94);
			Logger.info("JoeDemo: loading model end. countPageSize = {}/{}, countAllocSize = {}/{}",
					UnsafeUtil.allocPageSize(), UnsafeUtil.allocPageCount(),
					UnsafeUtil.allocMemSize(), UnsafeUtil.allocMemCount());
		}

		@Override
		public @NotNull Mat4 getMVP() {
			return uMVP;
		}
	}

	public static final class DemoModelNode extends SceneModel<DemoModel> {
		public DemoModelNode(@Nullable String name) {
			super(name);
			model = new DemoModel();
		}

		@Override
		protected void onUpdate() {
			var t = System.currentTimeMillis();
			if (model != null)
				model.uTime = Math.abs(t % 2000 * 0.0005f - 0.5f) - 0.25f;
			setScaleX(Math.abs(t % 2000 * 0.001f - 1) + 0.5f);
			var camera = Engine.current().getSceneRoot().getChildNode("SceneCamera");
			if (camera != null)
				camera.setRotateZ(t % 36000 * 0.01f);
			super.onUpdate();
		}
	}

	public static final class TextModel extends Render.Model {
		public final Mat4 uMVP = new Mat4().identity();
		public final Vec4 uColor = new Vec4().set(1f, 1f, 0.5f, 1.5f);
		public final @NotNull Render.Texture2D uTex;
		public final transient @NotNull Font.TextBox textBox;

		public TextModel() {
			var resMgr = Engine.current().getResourceManager();
			_program = resMgr.getOrAddShader("font.vs", "font.fs", getClass());
			var font = Objects.requireNonNull(resMgr.getFont("default"));
			uTex = font.getTexture();
			textBox = font.createTextBox(20);
			_mesh = textBox.mesh();
			_count = textBox.vertexCount();
		}

		@Override
		public @NotNull Mat4 getMVP() {
			return uMVP;
		}
	}

	public static final class TextModelNode extends SceneModel<TextModel> {
		public TextModelNode(@Nullable String name) {
			super(name);
			model = new TextModel();
			setX(Engine.current().getRender().getWindowWidth() * -0.5f);
			setY(Engine.current().getRender().getWindowHeight() * 0.5f);
		}

		@Override
		protected void onUpdate() {
			if (model != null) {
				model.textBox.set(String.format("测试aim0 1~*\nfps: %d", Engine.current().getFps()), 512, 2);
				model._count = model.textBox.vertexCount();
			}
			super.onUpdate();
		}
	}

	public static final class VideoModel extends Render.Model {
		public final Mat4 uMVP = new Mat4().identity();
		public final @NotNull Mat4 uYUV2RGB;
		public final @NotNull Render.Texture2D uTexY, uTexU, uTexV;
		public final transient int uvH;

		public VideoModel(@NotNull ResourceVideo.Image image) {
			var engine = Engine.current();
			var resMgr = engine.getResourceManager();
			_program = resMgr.getOrAddShader("video.vs", "video.fs", getClass());
			_mesh = SceneModel.createQuadMesh(-320, 320, -180, 180);
			_count = 6;
			uYUV2RGB = image.createCoeffMat();
			final int uvW;
			switch (image.yuvFormat) {
			case ResourceVideo.Image.YUV_FORMAT_444:
				uvW = image.width;
				uvH = image.height;
				break;
			case ResourceVideo.Image.YUV_FORMAT_420:
				uvW = (image.width + 1) >> 1;
				uvH = (image.height + 1) >> 1;
				break;
			default:
				throw new UnsupportedOperationException("yuvFormat=" + image.yuvFormat);
			}
			if (image.yStride != image.width)
				throw new IllegalStateException("invalid yStride " + image.yStride + " != " + image.width);
			if (image.uStride != uvW)
				throw new IllegalStateException("invalid uStride " + image.uStride + " != " + uvW);
			if (image.vStride != uvW)
				throw new IllegalStateException("invalid vStride " + image.vStride + " != " + uvW);
			var render = engine.getRender();
			uTexY = render.createTexture2D(image.width, image.height, 1, 0, image.yData);
			uTexU = render.createTexture2D(uvW, uvH, 1, 0, image.uData);
			uTexV = render.createTexture2D(uvW, uvH, 1, 0, image.vData);
		}

		@Override
		public @NotNull Mat4 getMVP() {
			return uMVP;
		}
	}

	public static final class VideoModelNode extends SceneModel<VideoModel> {
		public @Nullable ResourceVideo video;
		public @Nullable ResourceVideo.Image curImage;
		public long playBeginTime;

		public VideoModelNode(@Nullable String name) {
			super(name);
		}

		public void play(@NotNull String path) {
			video = Objects.requireNonNull(Engine.current().getResourceManager().load(path, ResourceVideo.class));
			curImage = video.decodeImage(0);
			model = new VideoModel(curImage);
			playBeginTime = System.nanoTime();
		}

		@Override
		protected void onRender(@NotNull Render render) {
			super.onRender(render);
			if (model != null && video != null && curImage != null) {
				if (video.getCurFrameIdx() + 1 < video.getFrameCount()) {
					curImage = video.decodeNextImage();
					//noinspection SuspiciousNameCombination
					model.uTexY.putData(0, 0, curImage.yStride, curImage.height, curImage.yData);
					model.uTexU.putData(0, 0, curImage.uStride, model.uvH, curImage.uData);
					model.uTexV.putData(0, 0, curImage.vStride, model.uvH, curImage.vData);
					var waitMs = (long)(video.getCurFrameTime() * 1000) - (System.nanoTime() - playBeginTime) / 1_000_000;
					if (waitMs > 0) {
						try {
							Thread.sleep(waitMs);
						} catch (InterruptedException e) {
							UnsafeUtil.forceThrow(e);
						}
					}
				} else {
					video = null;
					curImage = null;
				}
			}
		}
	}

	private static void runEngine() {
		var engine = Engine.current();
		var initParams = new Render.InitParams();
		initParams.title = "JoeDemo";
		initParams.fullWindow = false;
		engine.init(new RenderGles3(), initParams, new InputGlfw(), new AudioMini());
		engine.getResourceManager().recollectAll("res");
		engine.getResourceManager().addFont("default", "LXGWWenKaiGB-Regular", 32, 4096);

		var sceneCameraNode = new SceneCamera("SceneCamera");
		var sceneModelNode = new DemoModelNode("SceneModel");
		var sceneVideoNode = new VideoModelNode("SceneVideo");
		var guiCameraNode = new SceneCamera("GuiCamera");
		var guiTextNode = new TextModelNode("GuiText");
		sceneCameraNode.setRenderNode(sceneModelNode).setOrtho(2 * (16f / 9), 2, 0, 10).setX(0.48f);
		guiCameraNode.setDepthTest(false).setOrtho(initParams.width, initParams.height, 0, 10);
		engine.getSceneRoot().addChild(sceneCameraNode);
		engine.getSceneRoot().addChild(sceneModelNode);
		engine.getSceneRoot().addChild(guiCameraNode);
		guiCameraNode.addChild(sceneVideoNode);
		guiCameraNode.addChild(guiTextNode);

		ResourceMapFile bgmRes;
		Audio.Sound bgmSound;
		if (engine.getResourceManager().load("demo.avifs") != null) {
			sceneVideoNode.play("demo.avifs");
			bgmRes = engine.getResourceManager().load("demo.opa", ResourceMapFile.class);
			if (bgmRes != null) {
				bgmSound = engine.getAudio().loadSound(bgmRes.getDataPtr(), bgmRes.getDataSize(), true);
				bgmSound.play();
			}
		}

		engine.run();
	}

	@SuppressWarnings("UnnecessaryModifier")
	public static void main(@SuppressWarnings("unused") @NotNull String @NotNull [] args) throws InterruptedException {
		ResourceManager resMgr;
		Cleaner cleaner;
		try {
			runEngine();
			resMgr = Engine.current().getResourceManager();
			cleaner = Engine.current().getCleaner();
		} finally {
			Engine.current().destroy();
		}
		Logger.info("JoeDemo: countPageSize = {}/{}, countAllocSize = {}/{}, cacheRes={}, cleanerRef={}+{}",
				UnsafeUtil.allocPageSize(), UnsafeUtil.allocPageCount(),
				UnsafeUtil.allocMemSize(), UnsafeUtil.allocMemCount(),
				resMgr.getResCacheCount(), cleaner.getRefCount(), Cleaner.getGlobalRefCount());
		System.gc(); // gc Engine
		Thread.sleep(1_000);
		cleaner.poll(); // clean Mat4 in Engine
		System.gc(); // gc and clean MemPool
		Thread.sleep(1_000);
		Logger.info("JoeDemo: countPageSize = {}/{}, countAllocSize = {}/{}, cacheRes={}, cleanerRef={}+{}",
				UnsafeUtil.allocPageSize(), UnsafeUtil.allocPageCount(),
				UnsafeUtil.allocMemSize(), UnsafeUtil.allocMemCount(),
				resMgr.getResCacheCount(), cleaner.getRefCount(), Cleaner.getGlobalRefCount());
		System.exit(0);
	}
}
